﻿using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
using NConsoler;

namespace Altairis.MailToolkit.SmtpShovel {

    internal class Program {
        internal const string EVENTLOG_NAME = "Application";
        internal const string EVENTLOG_SOURCE = "Altairis SMTP Shovel";
        private const int ERRORLEVEL_SUCCESS = 0;
        private const int ERRORLEVEL_FAILURE = 1;

        private static bool useEventLog = true;
        private static bool debugMode = false;

        private static void Main(string[] args) {
            Console.WriteLine("Altairis Mail Toolkit SMTP Shovel | ver. {0:4}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
            Console.WriteLine("Copyright (c) Altairis, 2013");
            Console.WriteLine();

            // Check event log writing
            TryEventLogWriting();

            // Run actions
            Consolery.Run();
        }

        [Action("Shovel mail to Microsoft SMTP Service")]
        public static void SmtpSvc(
            [Required(Description = "URI of the mail pickup handler")] string handlerUriString,
            [Required(Description = "Handler secret key")] string handlerSecretString,
            [Optional(@"C:\InetPub\MailRoot\pickup", "p", Description = "Mail pickup folder location")] string pickupFolderPath,
            [Optional(false, Description = "Show verbose error messages")] bool debug) {
            debugMode = debug;

            // Validate arguments
            Console.Write("Reading parameters...");
            Uri handlerUri;
            if (!Uri.TryCreate(handlerUriString, UriKind.Absolute, out handlerUri)) CrashExit("Invalid handler URI");
            byte[] handlerSecret = null;
            try { handlerSecret = Convert.FromBase64String(handlerSecretString); }
            catch (Exception) { CrashExit("Secret string is not valid Base64"); }
            if (!Directory.Exists(pickupFolderPath)) CrashExit("Mail pickup folder location is invalid");

            try {
                // Download and extract package
                var packageFileName = DownloadPackageFile(handlerUri, handlerSecret);
                if (string.IsNullOrEmpty(packageFileName)) return; // no work here
                var packageFolderName = ExtractPackage(packageFileName);

                // Move all files to pickup folder
                var fileList = new DirectoryInfo(packageFolderName).GetFiles();
                Console.Write("Moving {0} files:", fileList.Length);
                foreach (var fi in fileList) {
                    var fileName = string.Format("SmtpShovel-{0:N}.eml", Guid.NewGuid());
                    fi.MoveTo(Path.Combine(pickupFolderPath, fileName));
                    Console.Write(".");
                }
                Console.WriteLine("OK");

                // Delete folder
                Console.Write("Deleting package folder...");
                Directory.Delete(packageFolderName);
                Console.WriteLine("OK");

                // Delete package from web
                DeletePackageFile(handlerUri, handlerSecret);
            }
            catch (Exception ex) {
                CrashExit(ex, "Unhandled exception");
            }
        }

        private static string ExtractPackage(string packageFileName) {
            var packageFolderName = Path.Combine(Path.GetTempPath(), "Altairis.MailToolkit.SmtpShovel");
            Console.Write("Extracing package to {0}...", packageFolderName);
            Directory.CreateDirectory(packageFolderName);
            ZipFile.ExtractToDirectory(packageFileName, packageFolderName);
            Console.WriteLine("OK");

            // Delete package
            Console.Write("Deleting package...");
            File.Delete(packageFileName);
            Console.WriteLine("OK");
            return packageFolderName;
        }

        [Action("Shovel mail to XMail server")]
        public static void XMail(
            [Required(Description = "URI of the mail pickup handler")] string handlerUriString,
            [Required(Description = "Handler secret key")] string handlerSecretString,
            [Optional(@"C:\MailRoot\spool\local", "p", Description = "Local spool queue folder path")] string pickupFolderPath,
            [Optional(false, Description = "Show verbose error messages")] bool debug) {
            debugMode = debug;
            Console.WriteLine("Not implemented yet");
        }

        private static string DownloadPackageFile(Uri handlerUri, byte[] handlerSecret) {
            // Validate arguments
            if (handlerUri == null) throw new ArgumentNullException("handlerUri");
            if (handlerSecret == null) throw new ArgumentNullException("handlerSecret");

            // Create authentication string
            var authString = GetAuthenticationString(handlerSecret);

            // Create HTTP request
            Console.Write("Downloading package {0}...", handlerUri);
            var rq = HttpWebRequest.Create(handlerUri);
            rq.Method = WebRequestMethods.Http.Get;
            rq.Headers.Add("Authorize", authString);
            rq.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore);
            using (var rp = rq.GetResponse() as HttpWebResponse) {
                switch (rp.StatusCode) {
                    case HttpStatusCode.OK:
                        var tempFileName = Path.GetTempFileName();
                        Console.WriteLine("OK");
                        Console.Write("Saving file to {0}...", tempFileName);
                        using (var source = rp.GetResponseStream())
                        using (var target = File.Create(tempFileName, 8192)) {
                            source.CopyTo(target);
                        }
                        Console.WriteLine("OK");
                        return tempFileName;
                    case HttpStatusCode.NoContent:
                        Console.WriteLine("OK, no content");
                        return null;
                    case HttpStatusCode.Unauthorized:
                        CrashExit("Handler returned the 401 Unauthorized status code. Most likely causes: invalid secret or clock skew.");
                        return null;
                    case HttpStatusCode.Forbidden:
                        CrashExit("Handler returned the 403 Forbidden status code. Most likely causes: IP blocked.");
                        return null;
                    default:
                        CrashExit("Handler returned unknown status code: {0}", rp.StatusCode);
                        return null;
                }
            }
        }

        private static void DeletePackageFile(Uri handlerUri, byte[] handlerSecret) {
            // Validate arguments
            if (handlerUri == null) throw new ArgumentNullException("handlerUri");
            if (handlerSecret == null) throw new ArgumentNullException("handlerSecret");

            // Create authentication string
            var authString = GetAuthenticationString(handlerSecret);

            // Create HTTP request
            Console.Write("Deleting package {0}...", handlerUri);
            var rq = HttpWebRequest.Create(handlerUri);
            rq.Method = "DELETE";
            rq.Headers.Add("Authorize", authString);
            rq.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore);
            using (var rp = rq.GetResponse() as HttpWebResponse) {
                switch (rp.StatusCode) {
                    case HttpStatusCode.NoContent:
                        Console.WriteLine("OK, deleted");
                        break;

                    case HttpStatusCode.Unauthorized:
                        CrashExit("Handler returned the 401 Unauthorized status code. Most likely causes: invalid secret or clock skew.");
                        break;

                    case HttpStatusCode.Forbidden:
                        CrashExit("Handler returned the 403 Forbidden status code. Most likely causes: IP blocked.");
                        break;

                    default:
                        CrashExit("Handler returned unknown or unexpected status code: {0}", rp.StatusCode);
                        break;
                }
            }
        }

        private static string GetAuthenticationString(byte[] handlerSecret) {
            if (handlerSecret == null) throw new ArgumentNullException("handlerSecret");

            Console.Write("Computing authentication code...");
            var authString = "HMACSHA512;{0};{1}";
            using (var hmac = new System.Security.Cryptography.HMACSHA512(handlerSecret)) {
                var dt = DateTime.UtcNow.ToString("s");
                var mac = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(dt));
                authString = string.Format(authString, dt, Convert.ToBase64String(mac));
            }
            Console.WriteLine("OK");
            return authString;
        }

        private static void CrashExit(string message, params object[] args) {
            if (message == null) throw new ArgumentNullException("message");
            if (string.IsNullOrWhiteSpace(message)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "message");

            Console.WriteLine("Failed!");
            Console.WriteLine("ERROR: " + message, args);
            LogEvent(EventLogEntryType.Error, message, args);
            Environment.Exit(ERRORLEVEL_FAILURE);
        }

        private static void CrashExit(Exception ex, string message, params object[] args) {
            if (ex == null) throw new ArgumentNullException("ex");

            Console.WriteLine("Failed!");
            Console.WriteLine("ERROR: " + message, args);
            Console.WriteLine(ex.Message);
            if (debugMode) Console.WriteLine(ex.ToString());
            LogEvent(EventLogEntryType.Error, ex, message);
            Environment.Exit(ERRORLEVEL_FAILURE);
        }

        #region EventLog writing

        private static void TryEventLogWriting() {
            try {
                LogEvent(EventLogEntryType.Information,
                    "Altairis Mail Toolkit SMTP Shovel version {0:4} started with the following command line: {1}",
                    System.Reflection.Assembly.GetExecutingAssembly().GetName().Version,
                    Environment.CommandLine);
            }
            catch (System.Security.SecurityException) {
                useEventLog = false;
                var oldFgColor = Console.ForegroundColor;
                var oldBgColor = Console.BackgroundColor;
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.BackgroundColor = ConsoleColor.Red;
                Console.WriteLine("                                                                               ");
                Console.WriteLine(" WARNING: This application is not correctly installed and therefore cannot use ");
                Console.WriteLine("          event logging system. Event logging is now suspended. Please install ");
                Console.WriteLine("          this application using the InstallUtil utility.                      ");
                Console.WriteLine("          InstallUtil.exe is part of .NET framework and must run under account ");
                Console.WriteLine("          with administrative privileges.                                      ");
                Console.WriteLine("                                                                               ");
                Console.BackgroundColor = oldBgColor;
                Console.ForegroundColor = oldFgColor;
                Console.WriteLine();
            }
        }

        public static void LogEvent(EventLogEntryType eventType, string message, params object[] args) {
            if (!useEventLog) return;

            // Create event source if necessary
            if (!EventLog.SourceExists(EVENTLOG_SOURCE)) EventLog.CreateEventSource(EVENTLOG_SOURCE, EVENTLOG_NAME);

            // Log event to eventlog
            EventLog.WriteEntry(EVENTLOG_SOURCE, string.Format(message, args), eventType);
        }

        public static void LogEvent(EventLogEntryType eventType, Exception exception, string message, params object[] args) {
            message += "\n\nException data:\n" + exception.ToString();
            LogEvent(eventType, message, args);
        }

        #endregion EventLog writing
    }
}